<script setup lang="ts">
import { ref } from 'vue';
import SparkMD5 from 'spark-md5';
import { useUpload, useMerge, useVerify } from '@/api/upload';
import { UPLOAD_CHUNK_SIZE, UPLOAD_CHUNK_AXIOS_MAX } from '@/constants/config';
import { UploadFilled } from '@element-plus/icons-vue';
import { UploadRequestHandler } from 'element-plus';

// 文件hash
const fileHash = ref<string>('');
// 文件名称
const fileName = ref<string>('');

// 切片大小，这里设置为5MB
const chunkSize = UPLOAD_CHUNK_SIZE;

/**
 * @description: 验证文件之前是否上传过
 * @return {*}
 */
const VerifyUpload = async () => {
	const params = {
		fileHash: fileHash.value,
		fileName: fileName.value,
	};
	const result = await useVerify(params);
	if (!result.shouldUpload) ElMessage.success('文件秒传成功');
	return result;
};

/**
 * @description: 文件分片
 * @param {*} file 文件
 * @return {*} 存放文件流的数组
 */
const createChunks = (file: File) => {
	// 文件分片到哪了
	let cur = 0;
	// 储存分片文件
	let chunks = [];

	// 循环切片
	while (cur < file.size) {
		const blod = file.slice(cur, cur + chunkSize);
		chunks.push(blod);
		cur += chunkSize;
	}
	return chunks;
};

/**
 * @description: 计算 hash 值
 * @param {*} chunks 分片文件数组
 */
const calculateHash = (chunks: Blob[]) => {
	return new Promise(resolve => {
		// 存储所有参与计算的切片
		const targets: Blob[] = [];
		const spark = new SparkMD5.ArrayBuffer();
		const fileReader = new FileReader();

		chunks.forEach((chunk, index) => {
			if (index === 0 || index === chunks.length - 1) {
				// 1. 第一个和最后一个切片全部参与计算
				targets.push(chunk);
			} else {
				// 2. 中间切片只计算前面两个字节、中间两个字节、最后两个字节

				// 前面两个字节
				targets.push(chunk.slice(0, 2));
				// 中间两个字节
				targets.push(chunk.slice(chunkSize / 2, chunkSize / 2 + 2));
				// 最后两个字节
				targets.push(chunk.slice(chunkSize - 2, 2));
			}
		});

		fileReader.readAsArrayBuffer(new Blob(targets));
		fileReader.onload = e => {
			spark.append((e.target as any).result);
			resolve(spark.end());
		};
	});
};

/**
 * @description: 文件合并
 */
const mergeRequest = async () => {
	const params = {
		size: chunkSize,
		fileHash: fileHash.value,
		fileName: fileName.value,
	};
	const result = await useMerge(params);
	if (!result) return ElMessage.error('切片合并未知异常');
	ElMessage.success(result);
};

/**
 * @description: 上传切片
 * @param {*} chunks 切片数组
 */
const uploadChunks = async (chunks: Blob[], existChunks: string[]) => {
	// 上传后端格式
	const data = chunks.map((chunk, index) => {
		return {
			fileHash: fileHash.value,
			chunkHash: fileHash.value + '-' + index,
			file: chunk,
		};
	});
	// 构建 formdata
	const formDatas = data
		// 过滤掉服务器已经存在的切片
		.filter(item => !existChunks.includes(item.chunkHash))
		.map(item => {
			const formData = new FormData();
			formData.append('fileHash', item.fileHash);
			formData.append('chunkHash', item.chunkHash);
			formData.append('file', item.file);
			return formData;
		});

	// 最大请求并发
	const max = UPLOAD_CHUNK_AXIOS_MAX;
	// 请求池
	const taskPool: any[] = [];
	// 当前上传第几个
	let index = 0;

	while (index < formDatas.length) {
		const task = useUpload(formDatas[index]);
		task.then(() => {
			taskPool.splice(taskPool.findIndex(item => item === task));
		});
		taskPool.push(task);
		if (taskPool.length === max) {
			await Promise.race(taskPool);
		}
		index++;
	}
	await Promise.all(taskPool);
	mergeRequest();
};

// 自定义上传文件
const httpRequest: UploadRequestHandler = config => {
	return new Promise(async (resolve, reject) => {
		// 在这里执行文件上传逻辑
		// config.file 包含了要上传的文件
		// config.data 包含了附加的数据
		// config.headers 包含了 HTTP 请求头部
		// config.withCredentials 是布尔值，用于指示是否使用凭据进行跨域请求
		// config.onProgress 是一个回调函数，用于处理上传进度
		// config.onSuccess 是一个回调函数，用于处理上传成功
		// config.onError 是一个回调函数，用于处理上传失败
		// 文件
		const { file } = config;
		// 分片
		const chunks = createChunks(file);
		// hash
		const hash = await calculateHash(chunks);
		fileHash.value = hash as string;
		fileName.value = file.name;
		const fileData = await VerifyUpload();
		if (!fileData.shouldUpload) return;
		uploadChunks(chunks, fileData.existChunks);
	});
};
</script>

<template>
	<div class="upload">
		<el-upload class="upload-demo" drag :show-file-list="false" :http-request="httpRequest">
			<el-icon class="el-icon--upload"><upload-filled /></el-icon>
			<div class="el-upload__text">将文件拖到此处或 <em>单击以上载</em></div>
		</el-upload>
	</div>
</template>

<style lang="less" scoped>
@import url('./index.less');
</style>
